Appearance
《C++面向对象程序设计(甲)》期末试卷B (精选02)
注意:答案直接写在答题纸上,答在试卷上无效,考试后答题纸和试卷一同上交
一、单项选择题(每题 2 分,总计 20 分)
- 面向对象程序设计中的数据隐藏指的是 ( )
- A. 输入数据必须输入保密口令
- B. 数据经过加密处理
- C. 对象内部数据和代码合并在一起
- D. 对象内部数据结构的不可访问性
查看答案与解析
答案:D
解析: 本题考查面向对象程序设计中的数据隐藏概念。
第一步:理解数据隐藏的含义 数据隐藏是面向对象程序设计的基本特征之一,指的是对象内部的数据结构(私有成员)对外部是不可见的,外部只能通过对象提供的公有接口(成员函数)来访问和操作数据。
第二步:逐一分析选项
- A 选项:输入数据必须输入保密口令——这是安全性措施,不是数据隐藏的核心含义。
- B 选项:数据经过加密处理——加密是数据安全技术,与面向对象的数据隐藏无关。
- C 选项:对象内部数据和代码合并在一起——这描述的是封装的概念,但不准确。封装是将数据和操作数据的方法绑定在一起,而数据隐藏强调的是"不可访问性"。
- D 选项:对象内部数据结构的不可访问性——正确,这正是数据隐藏的核心:外部不能直接访问对象的内部数据,只能通过公有接口间接访问。
方法总结:数据隐藏 = 私有成员对外不可见 + 通过公有接口访问。
难度: ⭐ 考点: #数据隐藏 #面向对象基本特征 #封装
💡 学习锦囊
📖 相关公式与知识点:
- 面向对象的三大基本特征:封装、继承、多态
- 封装 = 数据隐藏 + 公有接口
- 数据隐藏通过
private和protected访问控制实现 - 公有接口通过
public成员函数提供
思路分析
数据隐藏的关键词是"不可访问性"——外部代码不能直接读写对象的私有数据成员,必须通过类提供的公有方法。
易错点
- 容易将"数据隐藏"与"加密"混淆——加密是安全技术,数据隐藏是设计原则
- 容易将"封装"等同于"数据隐藏"——封装包含数据隐藏,但范围更广
🔄 举一反三
- 下列关于封装的描述中,正确的是 ( )
- A. 封装就是将所有数据成员都设为 private
- B. 封装是将数据和操作数据的方法绑定在一起,并隐藏内部实现细节
- C. 封装要求所有成员函数都必须是 public 的
- D. 封装就是数据隐藏
查看练习答案与解析
答案:B 解析:封装是将数据(属性)和操作数据的方法(行为)绑定在一起形成一个类,同时隐藏内部实现细节,只暴露必要的接口。A 错误因为封装不一定所有数据都是 private;C 错误因为成员函数可以是 private 或 protected;D 错误因为数据隐藏只是封装的一部分。
- 一个内联函数 Fun,使用 int 类型的参数,求其平方并返回,返回值也为 int 类型,下列定义正确的是 ( )
- A.
int Fun(int x){return x*x;} - B.
inline int Fun(int x){return x*x;} - C.
int Fun inline(int x){return x*x;} - D.
int Fun(int x){inline return x*x;}
- A.
查看答案与解析
答案:B
解析: 本题考查内联函数的定义语法。
第一步:回顾内联函数的定义方式 内联函数使用关键字 inline 来声明,语法格式为:
inline 返回值类型 函数名(参数列表) { 函数体 }第二步:逐一分析选项
- A 选项:
int Fun(int x){return x*x;}——没有inline关键字,只是普通函数定义。 - B 选项:
inline int Fun(int x){return x*x;}——正确,inline在返回值类型之前,符合标准语法。 - C 选项:
int Fun inline(int x){return x*x;}——inline放在函数名和参数列表之间,语法错误。 - D 选项:
int Fun(int x){inline return x*x;}——inline放在函数体内部,语法错误。
第三步:内联函数的工作原理 内联函数在编译时会在调用处展开函数体代码,避免了函数调用的开销(参数压栈、跳转等),适用于短小、频繁调用的函数。
难度: ⭐ 考点: #内联函数 #inline关键字 #函数定义语法
💡 学习锦囊
📖 相关公式与知识点:
- 内联函数定义:
inline 返回类型 函数名(参数) { 函数体 } - 内联函数适用于短小(通常 1-5 行)、频繁调用的函数
- 类体内定义的成员函数自动成为内联函数
inline是对编译器的建议,编译器可能忽略
思路分析
记住 inline 关键字的正确位置:在返回值类型之前,即 inline int Fun(...)。
易错点
- 将
inline放在函数体内部(如 D 选项) - 认为类外定义的成员函数加
inline也必须放在类声明中——实际上在类外定义时加inline也可以
🔄 举一反三
- 下列关于内联函数的描述中,错误的是 ( )
- A. 内联函数在编译时进行代码展开
- B. 内联函数可以减少函数调用的开销
- C. 所有函数都应该定义为内联函数以提高效率
- D. 递归函数通常不适合定义为内联函数
查看练习答案与解析
答案:C 解析:内联函数会导致代码膨胀(每个调用点都展开一份代码),因此只适合短小函数。如果所有函数都定义为内联函数,可执行文件会变得非常大,反而降低效率。递归函数无法在编译时确定展开次数,不适合内联。
- 下面关于重载函数的叙述中正确的是 ( )
- A. 重载函数必须具有不同的返回值类型
- B. 重载函数的形参个数必须不同
- C. 重载函数必须有不同的形参列表
- D. 重载函数的函数名可以不同
查看答案与解析
答案:C
解析: 本题考查函数重载的判定条件。
第一步:回顾函数重载的定义 函数重载是指在同一个作用域内,多个函数具有相同的函数名,但形参列表(参数个数、类型或顺序)不同。
第二步:逐一分析选项
- A 选项:重载函数必须具有不同的返回值类型——错误。返回值类型不能作为重载的区分条件,因为调用函数时可以不使用返回值。
- B 选项:重载函数的形参个数必须不同——错误。形参个数可以相同,只要类型或顺序不同即可。例如
void f(int)和void f(double)参数个数相同但类型不同,构成重载。 - C 选项:重载函数必须有不同的形参列表——正确。这是重载的核心条件。
- D 选项:重载函数的函数名可以不同——错误。重载的前提是函数名相同。
第三步:重载的判定规则 编译器根据函数调用时实参的类型和数量,匹配对应的重载版本。仅返回值类型不同不构成重载。
难度: ⭐ 考点: #函数重载 #形参列表 #重载判定条件
💡 学习锦囊
📖 相关公式与知识点:
- 重载条件:函数名相同 + 形参列表不同(个数/类型/顺序)
- 返回值类型不能区分重载
- 形参的
const修饰可以区分重载(如void f(int&)和void f(const int&)) - 重载解析:编译器根据实参匹配最佳重载版本
思路分析
记住口诀:"同名不同参"——函数名相同,参数列表不同。
易错点
- 认为返回值类型不同就可以重载——这是最常见的错误
- 忘记
const成员函数也可以构成重载
🔄 举一反三
- 下列函数中,能与
void f(int a)构成重载的是 ( )- A.
int f(int a) - B.
void f(int a, int b) - C.
void g(int a) - D.
int f(double a)
查看练习答案与解析
答案:B 和 D 解析:B 参数个数不同(1 个 vs 2 个),构成重载;D 参数类型不同(int vs double),构成重载。A 仅返回值类型不同,不构成重载;C 函数名不同(g vs f),不构成重载。
- A.
- 下列关于纯虚函数的描述中,错误的是 ( )
- A. 在基类中只有声明,通常没有函数体定义
- B. 可以使用包含纯虚函数的类来创建对象
- C. 当需要使用包含纯虚函数的基类的派生类创建对象时,须在派生类中给出该函数定义
- D. 包含纯虚函数的类称为抽象类
查看答案与解析
答案:B
解析: 本题考查纯虚函数和抽象类的概念。
第一步:回顾纯虚函数的定义 纯虚函数是在基类中声明但不实现的虚函数,语法为:virtual 返回类型 函数名(参数) = 0;
第二步:逐一分析选项
- A 选项:在基类中只有声明,通常没有函数体定义——正确。纯虚函数在基类中通过
=0声明,通常不提供函数体(但 C++ 允许在类外为纯虚函数提供定义)。 - B 选项:可以使用包含纯虚函数的类来创建对象——错误。包含纯虚函数的类是抽象类,不能实例化(创建对象)。
- C 选项:派生类需要给出纯虚函数的定义才能创建对象——正确。派生类必须实现(override)所有纯虚函数,才能成为具体类并创建对象。
- D 选项:包含纯虚函数的类称为抽象类——正确。
第三步:抽象类的特性 抽象类不能实例化,但可以定义指针和引用,用于实现多态。
难度: ⭐ 考点: #纯虚函数 #抽象类 #虚函数
💡 学习锦囊
📖 相关公式与知识点:
- 纯虚函数语法:
virtual void func() = 0; - 抽象类 = 包含至少一个纯虚函数的类
- 抽象类不能实例化(不能创建对象)
- 派生类必须实现所有纯虚函数才能实例化
- 抽象类可以定义指针和引用
思路分析
看到"纯虚函数"→ 想到"=0"→ 想到"抽象类"→ 想到"不能创建对象"。
易错点
- 混淆虚函数和纯虚函数——虚函数有函数体,纯虚函数没有
- 认为抽象类完全不能使用——实际上可以定义指针/引用
🔄 举一反三
- 下列关于抽象类的描述中,正确的是 ( )
- A. 抽象类中不能定义构造函数
- B. 抽象类不能定义数据成员
- C. 抽象类可以作为基类被继承
- D. 抽象类中的所有函数都必须是纯虚函数
查看练习答案与解析
答案:C 解析:抽象类可以作为基类被继承,这是它的主要用途——定义接口规范。A 错误,抽象类可以有构造函数(供派生类调用);B 错误,抽象类可以有数据成员;D 错误,抽象类中可以有非纯虚函数(普通成员函数)。
- 下列关于析构函数的描述中正确的是 ( )
- A. 析构函数可以重载
- B. 析构函数可以是虚函数
- C. 析构函数名与类名相同
- D. 析构函数的返回类型为 void
查看答案与解析
答案:B
解析: 本题考查析构函数的特性。
第一步:回顾析构函数的基本特征 析构函数在对象生命周期结束时自动调用,用于释放资源。其特征包括:
- 函数名:
~类名 - 无返回值类型(连 void 也不写)
- 无参数,因此不能重载
- 一个类只有一个析构函数
第二步:逐一分析选项
- A 选项:析构函数可以重载——错误。析构函数没有参数,无法重载。
- B 选项:析构函数可以是虚函数——正确。将基类的析构函数声明为虚函数,可以确保通过基类指针删除派生类对象时,正确调用派生类的析构函数。
- C 选项:析构函数名与类名相同——错误。析构函数名是
~类名,不是类名本身。 - D 选项:析构函数的返回类型为 void——错误。析构函数没有返回类型。
第三步:虚析构函数的重要性 当通过基类指针删除派生类对象时,如果析构函数不是虚函数,只会调用基类的析构函数,可能导致派生类资源泄漏。
难度: ⭐ 考点: #析构函数 #虚析构函数 #资源管理
💡 学习锦囊
📖 相关公式与知识点:
- 析构函数格式:
~类名() { ... } - 析构函数无参数、无返回值、不可重载
- 虚析构函数:
virtual ~类名() { ... } - 基类析构函数建议声明为虚函数
- 析构函数调用顺序:先派生类析构,后基类析构
思路分析
析构函数的关键特征:~ 开头、无参、无返回值、唯一。虚析构函数是多态正确释放资源的关键。
易错点
- 认为析构函数名与类名相同——实际是
~类名 - 忘记将基类析构函数声明为虚函数——导致派生类资源泄漏
🔄 举一反三
- 下列代码的输出结果是什么?cpp
class Base { public: ~Base() { cout << "Base destructor" << endl; } }; class Derived : public Base { public: ~Derived() { cout << "Derived destructor" << endl; } }; int main() { Base* p = new Derived(); delete p; return 0; }1
2
3
4
5
6
7
8
9
10
11
12
13查看练习答案与解析
答案:只输出
Base destructor解析:因为 Base 的析构函数不是虚函数,通过基类指针delete时只调用 Base 的析构函数,Derived 的析构函数不会被调用。将~Base()改为virtual ~Base()后,输出为Derived destructor然后Base destructor。
- 下列关于纯虚函数的描述中,正确的是 ( )
- A. 纯虚函数是一种特殊的虚函数,它是个空函数
- B. 具有纯虚函数的类称为虚基类
- C. 一个基类中说明有纯虚函数,其派生类一定要实现该纯虚函数
- D. 具有纯虚函数的类不能创建类对象
查看答案与解析
答案:D
解析: 本题考查纯虚函数和抽象类的正确理解。
第一步:逐一分析选项
- A 选项:纯虚函数是个空函数——错误。纯虚函数是"没有实现"的函数(=0),不是"空函数"(空函数有函数体
{})。 - B 选项:具有纯虚函数的类称为虚基类——错误。具有纯虚函数的类称为"抽象类";"虚基类"是解决菱形继承问题的概念(
virtual继承)。 - C 选项:派生类一定要实现该纯虚函数——错误。派生类可以不实现纯虚函数,但此时派生类也是抽象类,不能实例化。
- D 选项:具有纯虚函数的类不能创建类对象——正确。这是抽象类的核心特性。
第二步:区分关键概念
- 纯虚函数 ≠ 空函数:
virtual void f() = 0;(纯虚函数)vsvirtual void f() {}(空实现的虚函数) - 抽象类 ≠ 虚基类:抽象类(含纯虚函数)vs 虚基类(virtual 继承的基类)
难度: ⭐ 考点: #纯虚函数 #抽象类 #虚基类
💡 学习锦囊
📖 相关公式与知识点:
- 纯虚函数:
virtual void f() = 0;——只有声明,没有实现 - 空函数:
virtual void f() {}——有实现,但函数体为空 - 抽象类:包含纯虚函数的类,不能实例化
- 虚基类:使用
virtual继承的基类,解决菱形继承
思路分析
区分三个易混淆概念:纯虚函数(=0)、抽象类(含纯虚函数)、虚基类(virtual 继承)。
易错点
- 混淆"抽象类"和"虚基类"——两个完全不同的概念
- 认为派生类必须实现纯虚函数——可以不实现,但自身也成为抽象类
🔄 举一反三
- 下列关于虚基类的描述中,正确的是 ( )
- A. 虚基类就是包含纯虚函数的类
- B. 虚基类用于解决多重继承中的二义性问题
- C. 虚基类不能有数据成员
- D. 虚基类的构造函数不会被调用
查看练习答案与解析
答案:B 解析:虚基类(virtual base class)用于解决菱形继承中基类成员的多份拷贝问题。A 错误,包含纯虚函数的是抽象类;C 错误,虚基类可以有数据成员;D 错误,虚基类的构造函数由最派生类负责调用。
- 复制初始化构造函数的作用是 ( )
- A. 进行数据类型的转换
- B. 用对象调用成员函数
- C. 用对象初始化对象
- D. 用一般类型的数据初始化对象
查看答案与解析
答案:C
解析: 本题考查复制(拷贝)构造函数的用途。
第一步:回顾拷贝构造函数的定义 拷贝构造函数是用一个已存在的同类对象来初始化新创建的对象。其典型声明为:
ClassName(const ClassName& other);第二步:逐一分析选项
- A 选项:进行数据类型的转换——这是转换构造函数的作用(单参数非拷贝构造函数)。
- B 选项:用对象调用成员函数——这是普通成员函数调用的作用。
- C 选项:用对象初始化对象——正确。这正是拷贝构造函数的用途。
- D 选项:用一般类型的数据初始化对象——这是普通构造函数或转换构造函数的作用。
第三步:拷贝构造函数的调用场景
- 用已有对象初始化新对象:
ClassName obj2 = obj1; - 函数参数按值传递对象时
- 函数返回值为对象时
难度: ⭐ 考点: #拷贝构造函数 #对象初始化 #深拷贝与浅拷贝
💡 学习锦囊
📖 相关公式与知识点:
- 拷贝构造函数声明:
ClassName(const ClassName& obj); - 默认拷贝构造函数执行浅拷贝(逐成员复制)
- 当类包含指针成员时,需要自定义深拷贝
- 调用场景:初始化新对象、值传递参数、值返回对象
思路分析
拷贝构造函数 = "用同类对象初始化新对象"。关键词是"初始化"(区别于"赋值")。
易错点
- 混淆拷贝构造函数和赋值运算符——拷贝构造是初始化,赋值是已有对象的赋值
- 忘记
const引用参数——应该是const ClassName&
🔄 举一反三
- 下列哪种情况不会调用拷贝构造函数? ( )
- A.
MyClass obj2 = obj1; - B.
void func(MyClass obj);调用func(obj1); - C.
obj2 = obj1;(obj2 已存在) - D.
return obj1;(函数返回值)
查看练习答案与解析
答案:C 解析:C 是赋值操作,调用的是赋值运算符
operator=,不是拷贝构造函数。A 是初始化,调用拷贝构造;B 是值传递参数,调用拷贝构造;D 是返回值,调用拷贝构造。 - A.
- 所谓多态性是指 ( )
- A. 不同的对象调用不同名称的函数
- B. 不同的对象调用相同名称的函数
- C. 一个对象调用不同名称的函数
- D. 一个对象调用不同名称的对象
查看答案与解析
答案:B
解析: 本题考查面向对象中多态性的概念。
第一步:理解多态的含义 多态(Polymorphism)指的是同一个接口(函数名),不同的对象调用时表现出不同的行为。即"一个接口,多种实现"。
第二步:逐一分析选项
- A 选项:不同对象调用不同名称的函数——这只是普通的函数调用,不是多态。
- B 选项:不同的对象调用相同名称的函数——正确。通过虚函数机制,基类指针/引用调用同名函数时,根据实际对象类型执行不同的函数版本。
- C 选项:一个对象调用不同名称的函数——普通成员函数调用。
- D 选项:一个对象调用不同名称的对象——语法不通。
第三步:多态的两种形式
- 编译时多态(静态多态):函数重载、运算符重载、模板
- 运行时多态(动态多态):虚函数 + 基类指针/引用
难度: ⭐ 考点: #多态性 #虚函数 #动态绑定
💡 学习锦囊
📖 相关公式与知识点:
- 多态 = 同一接口,不同实现
- 静态多态:函数重载、模板(编译时确定)
- 动态多态:虚函数 + 基类指针/引用(运行时确定)
- 动态多态三要素:继承、虚函数、基类指针/引用
思路分析
多态的核心:"同名函数,不同行为"。通过基类指针调用虚函数时,实际执行的是派生类的版本。
易错点
- 认为函数重载就是多态的全部——重载只是静态多态
- 忘记多态需要基类指针/引用——直接通过对象调用不会触发多态
🔄 举一反三
- 下列代码的输出结果是什么?cpp
class Base { public: virtual void show() { cout << "Base" << endl; } }; class Derived : public Base { public: void show() { cout << "Derived" << endl; } }; int main() { Base b; Derived d; Base* p = &b; p->show(); p = &d; p->show(); return 0; }1
2
3
4
5
6
7
8
9
10
11
12
13
14查看练习答案与解析
答案:先输出
Base,再输出Derived解析:show()是虚函数,通过基类指针调用时发生动态绑定。p指向b时调用Base::show(),指向d时调用Derived::show()。这就是运行时多态。
- 要将类 A 说明是类 B 的虚基类,正确的描述是 ( )
- A.
class virtual B : public A - B.
class B : virtual public A - C.
virtual class B : public A - D.
class B : public A virtual
- A.
查看答案与解析
答案:B
解析: 本题考查虚基类的声明语法。
第一步:回顾虚基类的语法 虚继承使用 virtual 关键字,放在继承方式之前:
class 派生类 : virtual 继承方式 基类 { ... };第二步:逐一分析选项
- A 选项:
class virtual B : public A——virtual位置错误,不能放在class关键字之后。 - B 选项:
class B : virtual public A——正确。virtual在继承方式public之前。 - C 选项:
virtual class B : public A——virtual位置错误。 - D 选项:
class B : public A virtual——virtual位置错误,不能放在最后。
第三步:虚基类的作用 虚基类用于解决菱形继承(钻石继承)问题,确保最终派生类中只有一份基类成员的拷贝。
难度: ⭐ 考点: #虚基类 #虚继承 #菱形继承
💡 学习锦囊
📖 相关公式与知识点:
- 虚继承语法:
class B : virtual public A { }; - 虚基类解决菱形继承中的二义性和数据冗余
- 虚基类的构造函数由最派生类负责调用
virtual关键字在继承方式之前
思路分析
记住语法格式:class 派生类 : virtual 继承方式 基类。virtual 在继承方式前面。
易错点
virtual的位置放错——必须在继承方式之前- 混淆虚基类和抽象类——虚基类用
virtual继承,抽象类含纯虚函数
🔄 举一反三
- 在菱形继承结构中,如果不使用虚基类,最终派生类中会包含几份基类成员的拷贝? ( )
- A. 1 份
- B. 2 份
- C. 3 份
- D. 4 份
查看练习答案与解析
答案:B 解析:在典型的菱形继承中(B 和 C 都继承 A,D 同时继承 B 和 C),如果不使用虚基类,D 中会包含 2 份 A 的成员拷贝(分别来自 B 和 C 路径)。使用虚基类后,D 中只有 1 份 A 的成员拷贝。
- 标准模板库(STL)所涉及的 4 个最主要的基本组件是 ( )
- A. 容器、迭代器、算法、函数模板
- B. 类模板、运算符重载函数、容器、算法
- C. 容器、算法、迭代器、函数对象
- D. 类、对象、迭代器、函数
查看答案与解析
答案:C
解析: 本题考查 STL(Standard Template Library)的核心组件。
第一步:回顾 STL 的四大组件 STL 由以下四个核心组件构成:
- 容器(Containers):存储数据的模板类,如
vector、list、map - 算法(Algorithms):操作数据的模板函数,如
sort、find、copy - 迭代器(Iterators):连接容器和算法的"胶水",提供遍历容器的方法
- 函数对象(Functors):重载了
operator()的类对象,可作为算法的策略参数
第二步:逐一分析选项
- A 选项:将"函数对象"误写为"函数模板"——错误。
- B 选项:类模板和运算符重载不是 STL 的核心组件——错误。
- C 选项:容器、算法、迭代器、函数对象——正确。
- D 选项:类、对象是面向对象的基本概念,不是 STL 组件——错误。
难度: ⭐ 考点: #STL #标准模板库 #容器 #迭代器 #算法
💡 学习锦囊
📖 相关公式与知识点:
- STL 四大组件:容器、算法、迭代器、函数对象
- 常用容器:
vector(动态数组)、list(双向链表)、map(键值对)、set(集合) - 常用算法:
sort、find、for_each、copy - 迭代器类型:输入、输出、前向、双向、随机访问
- 函数对象:重载
operator()的类
思路分析
STL = 容器(存数据)+ 算法(处理数据)+ 迭代器(遍历数据)+ 函数对象(定制行为)。
易错点
- 将"函数对象"记成"函数模板"——函数模板是泛型编程的基础,但不是 STL 四大组件之一
- 忘记迭代器是独立组件——迭代器是容器和算法之间的桥梁
🔄 举一反三
- 下列关于 STL 容器的描述中,错误的是 ( )
- A.
vector支持随机访问 - B.
list支持随机访问 - C.
map存储键值对 - D.
set中的元素自动排序
查看练习答案与解析
答案:B 解析:
list是双向链表,不支持随机访问(不能使用[]运算符),只能通过迭代器顺序访问。vector是动态数组,支持随机访问;map存储键值对并按键自动排序;set存储唯一元素并自动排序。 - A.
二、程序填空题(每空 2 分,总计 20 分)
- 请在下面程序的横线处填上适当内容,以使程序完整,并使程序的输出为:
2,1
4,32
#include <iostream>
using namespace std;
class A
{
int a;
public:
A(int i = 0) { a = i; }
int Geta() { return a; }
};
class B
{
A a;
int b;
public:
B(int i = 0, int j = 0) : __(1)__ {}
void display() { cout << a.Geta() << ',' << b << endl; }
};
int main()
{
B b[2] = { B(1, 2), B(3, 4) };
for (int i = 0; i < 2; i++)
__(2)__
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
查看答案与解析
答案: (1) a(j), b(i) (2) b[i].display()
解析:
第 (1) 空分析:
- 类 B 中包含一个 A 类型的成员对象
a和一个 int 类型的成员b。 - 在初始化列表中,需要用参数
j初始化成员对象a(调用 A 的构造函数A(j)),用参数i初始化成员b。 - 初始化列表语法:
B(int i, int j) : a(j), b(i) {},其中a是 A 类成员,用j初始化;b是 int 成员,用i初始化。
第 (2) 空分析:
- 循环遍历数组
b的两个元素,需要调用display()输出结果。 - 语法:
b[i].display();
难度: ⭐⭐ 考点: #成员初始化列表 #对象数组成员 #组合关系
💡 学习锦囊
📖 相关公式与知识点:
- 成员初始化列表语法:
构造函数(参数) : 成员1(值1), 成员2(值2) { } - 对象数组成员:
ClassName arr[2] = { obj1, obj2 }; - 成员对象必须在初始化列表中初始化(因为成员对象的构造先于包含类的构造体)
思路分析
看到初始化列表中的填空 → 思考哪些成员需要初始化 → 对象成员(A 类)和基本类型成员(int b)。
易错点
- 初始化列表顺序应与成员声明顺序一致
- 忘记对象成员必须在初始化列表中初始化
🔄 举一反三
- 下列代码的输出结果是什么?cpp
class X { int val; public: X(int v) : val(v) { cout << "X(" << val << ") "; } }; class Y { X x; public: Y(int a, int b) : x(a) { cout << "Y(" << b << ")"; } }; int main() { Y y(10, 20); return 0; }1
2
3
4
5
6
7
8
9
10
11查看练习答案与解析
答案:
X(10) Y(20)解析:创建 Y 对象时,先执行初始化列表中的x(a)(即X(10)),输出X(10),然后执行 Y 构造函数体,输出Y(20)。成员对象先于包含类构造。
- 下面程序中 A 是抽象类。请在下面程序的横线处填上适当内容,以使程序完整,并使程序的输出为:
B1 called
B2 called2
#include <iostream>
using namespace std;
class A
{
public:
__(3)__
};
class B1 : public A
{
public:
void display() { cout << "B1 called" << endl; }
};
class B2 : public A
{
public:
void display() { cout << "B2 called" << endl; }
};
void show(__(4)__)
{
p->display();
}
int main()
{
B1 b1;
B2 b2;
A* p[2] = { &b1, &b2 };
for (int i = 0; i < 2; i++)
show(p[i]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
查看答案与解析
答案: (3) virtual void display() = 0; (4) A *p
解析:
第 (3) 空分析:
- 题目说明"A 是抽象类",抽象类必须包含至少一个纯虚函数。
- 派生类 B1、B2 都实现了
display()函数,说明基类 A 中声明了纯虚函数display()。 - 纯虚函数语法:
virtual void display() = 0;
第 (4) 空分析:
show函数体中使用了p->display(),说明参数p是一个指针。main中调用show(p[i]),p[i]的类型是A*。- 因此参数类型应为
A*,即void show(A *p)。
难度: ⭐⭐ 考点: #纯虚函数 #抽象类 #多态 #基类指针
💡 学习锦囊
📖 相关公式与知识点:
- 纯虚函数声明:
virtual 返回类型 函数名(参数) = 0; - 抽象类必须包含至少一个纯虚函数
- 通过基类指针实现运行时多态
- 基类指针可以指向派生类对象
思路分析
抽象类 → 纯虚函数 = 0 → 派生类实现 → 基类指针调用 → 多态。
易错点
- 忘记
= 0的语法——纯虚函数必须有= 0 - 参数类型写成
B1*或B2*——应该用基类指针A*实现通用性
🔄 举一反三
- 如果 A 类中还有一个纯虚函数
virtual void show() = 0;,而 B1 没有实现它,B1 能实例化吗?查看练习答案与解析
答案:不能 解析:派生类必须实现基类的所有纯虚函数才能成为具体类(可实例化)。如果 B1 没有实现
show(),B1 也是抽象类,不能创建 B1 对象。
- 请在下面程序的横线处填上适当内容,以使程序完整,并使程序的输出为:
Name:王小明
Grade:902
#include <iostream>
#include <cstring>
class Person
{
char name[20];
public:
Person(char* s) { strcpy(name, s); }
void display() { cout << "Name:" << name << endl; }
};
class Student : public Person
{
int grade;
public:
Student(char* s, int g) : __(5)__ { grade = g; }
void display()
{
__(6)__
cout << "Grade:" << grade << endl;
}
};
int main()
{
Student s("王小明", 90);
s.display();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
查看答案与解析
答案: (5) Person(s) (6) Person::display();
解析:
第 (5) 空分析:
Student继承自Person,Student构造函数需要初始化基类Person。Person的构造函数需要一个char*参数,Student构造函数接收了char* s。- 在初始化列表中调用基类构造函数:
Person(s)。
第 (6) 空分析:
Student::display()需要先输出姓名(基类功能),再输出成绩。- 调用基类的
display()函数:Person::display(); - 使用作用域限定符
::明确指定调用基类版本。
难度: ⭐⭐ 考点: #继承 #基类构造调用 #作用域限定符
💡 学习锦囊
📖 相关公式与知识点:
- 派生类构造函数初始化列表调用基类构造:
Student(...) : Person(s) { } - 调用基类被覆盖的成员函数:
Person::display(); - 派生类对象构造顺序:基类 → 成员对象 → 派生类本身
- 派生类对象析构顺序:派生类 → 成员对象 → 基类(与构造相反)
思路分析
派生类构造 → 先初始化基类(初始化列表)→ 派生类中调用基类方法 → 使用 基类名::方法名()
易错点
- 在构造函数体内给基类成员赋值——基类构造应在初始化列表中调用
- 直接写
display()而不加Person::——会递归调用自身
🔄 举一反三
- 下列代码的输出结果是什么?cpp
class Base { public: Base() { cout << "B "; } void show() { cout << "Base::show "; } }; class Derived : public Base { public: Derived() { cout << "D "; } void show() { cout << "Derived::show "; } }; int main() { Derived d; d.show(); d.Base::show(); return 0; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16查看练习答案与解析
答案:
B D Derived::show Base::show解析:构造 Derived 对象时先调用 Base 构造输出B,再执行 Derived 构造体输出D。d.show()调用 Derived 版本输出Derived::show。d.Base::show()通过作用域限定符调用基类版本输出Base::show。
- 请在下面程序的横线处填上适当内容,以使程序完整,并使程序的输出为
5。
#include <iostream>
using namespace std;
class Integer
{
int x;
public:
Integer(int a = 0) { x = a; }
void display() { cout << x << endl; }
__(7)__
};
Integer Max(Integer a, Integer b)
{
if (__(8)__)
return a;
return b;
}
int main()
{
Integer a(3), b(5), c;
c = Max(a, b);
c.display();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
查看答案与解析
答案: (7) friend Integer Max(Integer a, Integer b); (8) a.x > b.x
解析:
第 (7) 空分析:
Max函数在类外定义,但需要访问Integer的私有成员x。- 因此
Max需要被声明为Integer的友元函数。 - 友元函数声明语法:
friend 返回类型 函数名(参数列表); - 即:
friend Integer Max(Integer a, Integer b);
第 (8) 空分析:
Max函数需要比较两个Integer对象中x的值。- 由于
Max是友元函数,可以直接访问私有成员x。 - 比较表达式:
a.x > b.x
难度: ⭐⭐ 考点: #友元函数 #私有成员访问 #运算符重载
💡 学习锦囊
📖 相关公式与知识点:
- 友元函数声明:
friend 返回类型 函数名(参数); - 友元函数可以访问类的私有和保护成员
- 友元函数不是类的成员函数
- 友元关系是单向的、不可传递的、不可继承的
思路分析
类外函数需要访问私有成员 → 声明为友元 → friend 关键字 → 友元函数可直接访问 a.x。
易错点
- 忘记友元函数不是成员函数——不需要通过对象调用
- 友元声明放在类内部的任何位置都可以(public/private 无影响)
🔄 举一反三
- 如果将
Max改为成员函数,应该如何修改代码?查看练习答案与解析
答案:将
Max改为成员函数:cppInteger Max(Integer b) { if (x > b.x) return *this; return b; }1
2
3
4调用方式改为
c = a.Max(b);。成员函数只需要一个参数(另一个操作数是this),可以直接访问自己的x和参数的x(同类对象间可以访问私有成员)。
- 请在下面的横线处填上适当内容,以使类的定义完整。
class Array
{
int* ptr;
int size;
public:
Array() { size = 0; ptr = NULL; }
Array(int n) { size = n; ptr = new int[size]; }
Array(__(9)__) // 复制初始化构造函数
{
size = a.size;
ptr = new int[size];
for (int i = 0; i < size; i++)
__(10)__; // 将源对象的动态数组内容复制到目标对象
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
查看答案与解析
答案: (9) const Array &a (10) ptr[i] = a.ptr[i]
解析:
第 (9) 空分析:
- 这是拷贝构造函数的声明,参数应为同类对象的引用。
- 标准格式:
Array(const Array &a) const确保不修改源对象,&引用避免无限递归(值传递会再次调用拷贝构造)。
第 (10) 空分析:
- 需要将源对象的动态数组内容逐元素复制到目标对象。
- 语法:
ptr[i] = a.ptr[i]; - 这是深拷贝的关键步骤——不仅复制指针,而是复制指针指向的内容。
难度: ⭐⭐ 考点: #拷贝构造函数 #深拷贝 #动态内存管理
💡 学习锦囊
📖 相关公式与知识点:
- 拷贝构造函数参数必须是
const ClassName&(引用,避免无限递归) - 深拷贝 = 复制指针指向的内容(而非指针本身)
- 浅拷贝 = 只复制指针值(两个对象指向同一内存,可能导致重复释放)
- 当类包含动态分配的内存时,必须自定义拷贝构造函数(深拷贝)
思路分析
类有指针成员 → 需要深拷贝 → 拷贝构造函数参数为 const Array &a → 逐元素复制 ptr[i] = a.ptr[i]。
易错点
- 参数写成
Array a(值传递)——会导致无限递归调用拷贝构造函数 - 只写
ptr = a.ptr(浅拷贝)——两个对象指向同一内存,析构时重复释放
🔄 举一反三
- 如果使用浅拷贝(默认拷贝构造函数),下列代码会出现什么问题?cpp
Array a1(5); Array a2 = a1; // 浅拷贝 // a1 和 a2 的 ptr 指向同一块内存 // main 结束时 a1 和 a2 都会调用析构函数 delete[] ptr1
2
3
4查看练习答案与解析
答案:程序崩溃(double free) 解析:浅拷贝导致
a1.ptr和a2.ptr指向同一块动态内存。当a1和a2析构时,都会执行delete[] ptr,同一块内存被释放两次,导致运行时错误(double free)。深拷贝为每个对象分配独立的内存,避免此问题。
三、程序阅读题(每题 6 分,总计 30 分)
- 阅读该程序,给出程序的输出结果。
#include <iostream>
using namespace std;
class Test
{
private:
int num;
public:
Test(int n = 0) { num = n; num++; }
~Test() { cout << "Destructor is active, number = " << num << endl; }
};
int main()
{
Test x[2];
cout << "Exiting main" << endl;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
查看答案与解析
答案:
Exiting main
Destructor is active, number = 1
Destructor is active, number = 12
3
解析:
第一步:分析对象数组的构造Test x[2]; 创建了两个 Test 对象,都使用默认参数 n = 0:
x[0]:num = 0,然后num++→num = 1x[1]:num = 0,然后num++→num = 1
第二步:分析输出顺序
- 先执行
cout << "Exiting main" << endl;,输出Exiting main main函数结束时,对象数组x被销毁,调用析构函数- 析构顺序与构造顺序相反:先析构
x[1],再析构x[0] - 两个对象的
num都是 1,所以输出两次Destructor is active, number = 1
难度: ⭐⭐ 考点: #析构函数 #对象数组 #构造与析构顺序
💡 学习锦囊
📖 相关公式与知识点:
- 对象数组的每个元素都会调用构造函数
- 析构顺序与构造顺序相反(后构造的先析构)
- 默认参数在创建对象数组时仍然生效
- 局部对象在离开作用域时自动析构
思路分析
对象数组 → 每个元素独立构造 → 默认参数 n=0 → num++ 后 num=1 → main 结束 → 逆序析构。
易错点
- 认为
num++后num是 0——num++是后置自增,但赋值后num确实变为 1 - 忘记析构顺序是逆序的
🔄 举一反三
- 如果将
Test x[2];改为Test x[2] = { Test(5), Test(10) };,输出结果是什么?查看练习答案与解析
答案:
Exiting main Destructor is active, number = 11 Destructor is active, number = 61
2
3解析:
x[0]用n=5构造,num=5,num++后num=6;x[1]用n=10构造,num=10,num++后num=11。析构时逆序:先x[1](num=11),再x[0](num=6)。
- 阅读该程序,给出程序的输出结果。
#include <iostream>
using namespace std;
class A
{
public:
virtual void fun(int data) { cout << "class A:" << data << endl; }
void fun(char* str) { cout << "class A:" << str << endl; }
};
class B : public A
{
public:
void fun() { cout << "class B" << endl; }
void fun(int data) { cout << "class B:" << data << endl; }
void fun(char* str) { cout << "class B:" << str << endl; }
};
int main()
{
A* pA;
B b;
pA = &b;
pA->fun(1);
pA->fun("Hello");
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
查看答案与解析
答案:
class B:1
class A:Hello2
解析:
第一步:分析 pA->fun(1) 的调用
fun(int)在基类 A 中声明为virtual(虚函数)。pA是A*类型,指向 B 对象b。- 由于
fun(int)是虚函数,发生动态绑定,调用 B 中重写的版本。 - 输出:
class B:1
第二步:分析 pA->fun("Hello") 的调用
fun(char*)在基类 A 中没有virtual关键字,是普通成员函数。- 普通成员函数的调用在编译时确定(静态绑定),根据指针的静态类型
A*决定。 - 因此调用
A::fun(char*)。 - 输出:
class A:Hello
第三步:关键结论 只有声明为 virtual 的函数才会发生动态绑定(多态)。非虚函数通过基类指针调用时,执行基类版本。
难度: ⭐⭐⭐ 考点: #虚函数 #动态绑定 #静态绑定 #多态
💡 学习锦囊
📖 相关公式与知识点:
- 虚函数:使用
virtual关键字声明,支持动态绑定 - 非虚函数:编译时静态绑定,根据指针/引用的静态类型决定调用版本
- 动态绑定条件:虚函数 + 基类指针/引用 + 指向派生类对象
virtual关键字只在基类声明时需要,派生类中可写可不写
思路分析
判断函数是否为虚函数 → 是虚函数则动态绑定(调用派生类版本)→ 不是虚函数则静态绑定(调用基类版本)。
易错点
- 认为所有通过基类指针的调用都会发生多态——只有虚函数才会
- 混淆
fun(int)和fun(char*)——前者是虚函数,后者不是
🔄 举一反三
- 如果将
void fun(char* str)也加上virtual,输出结果是什么?查看练习答案与解析
答案:
class B:1 class B:Hello1
2解析:两个函数都变成虚函数后,通过基类指针调用时都发生动态绑定,执行 B 类中重写的版本。
- 阅读该程序,给出程序的输出结果。
#include <iostream>
using namespace std;
int main()
{
cout.fill('*');
cout.width(10);
cout << "123.45" << endl;
cout.width(8);
cout << "123.45" << endl;
cout.width(4);
cout << "123.45" << endl;
}2
3
4
5
6
7
8
9
10
11
12
13
查看答案与解析
答案:
****123.45
**123.45
123.452
3
解析:
第一步:理解 cout.fill() 和 cout.width()
cout.fill('*'):设置填充字符为*(默认是空格)。fill()设置后持续有效,直到被重新设置。cout.width(n):设置下一次输出的最小字段宽度为n。注意:width()只对下一次输出有效,之后恢复默认值 0。
第二步:逐行分析
cout.width(10):宽度 10,"123.45"长度为 6,左侧填充 4 个*→****123.45cout.width(8):宽度 8,"123.45"长度为 6,左侧填充 2 个*(fill('*')仍然有效)→**123.45cout.width(4):宽度 4 <"123.45"长度 6,不进行填充 →123.45
第三步:关键知识点
fill()设置的填充字符持续有效,不会因为width()的重设而恢复默认width()只对下一次输出有效,输出后自动恢复为 0- 当设置宽度小于数据实际宽度时,数据不会被截断
难度: ⭐⭐ 考点: #cout格式化 #width #fill #流操纵
💡 学习锦囊
📖 相关公式与知识点:
cout.width(n):设置下次输出的最小宽度,一次性有效cout.fill(ch):设置填充字符,持续有效直到再次设置cout.precision(n):设置浮点数精度cout.setf(ios::left):设置左对齐- 当数据实际宽度 ≥ 设置宽度时,不进行填充
思路分析
width() 只对紧接着的一次 << 输出有效,之后自动恢复为 0(不限制宽度)。
易错点
- 认为
width()设置后对所有后续输出都有效——实际上只对下一次有效 - 认为
fill()设置的填充字符会因width()重设而恢复默认——实际上fill()持续有效 - 当设置宽度小于数据实际宽度时,数据不会被截断
🔄 举一反三
- 下列代码的输出结果是什么?cpp
cout.fill('0'); cout.width(5); cout << 42 << endl; cout.width(5); cout << 7 << endl;1
2
3
4
5查看练习答案与解析
答案:
00042 000071
2解析:
fill('0')设置填充字符为0,持续有效。第一次width(5)输出 42(2 位),填充 3 个0→00042。第二次width(5)输出 7(1 位),填充 4 个0→00007。
- 从键盘输入 10,给出程序的输出结果。
#include <iostream>
#include <vector>
#include <iterator>
using namespace std;
int main()
{
vector<int> v;
int i, n;
cin >> n;
for (i = 1; i <= n; i++)
{
if (i % 2 != 0)
v.push_back(i);
}
vector<int>::iterator p;
for (p = v.begin(); p != v.end(); p++)
cout << *p << " ";
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
查看答案与解析
答案:
1 3 5 7 9解析:
第一步:理解程序逻辑
- 输入
n = 10 - 循环
i从 1 到 10 if (i % 2 != 0):当i是奇数时,将i添加到 vector 中- 因此 vector 中的元素为:1, 3, 5, 7, 9
第二步:分析输出
- 使用迭代器遍历 vector,输出每个元素后跟一个空格
- 输出:
1 3 5 7 9
第三步:关键知识点
vector::push_back():在末尾添加元素vector::begin()/vector::end():获取迭代器i % 2 != 0:判断奇数
难度: ⭐⭐ 考点: #STL #vector #迭代器 #push_back
💡 学习锦囊
📖 相关公式与知识点:
vector是动态数组,支持随机访问push_back(x):在 vector 末尾添加元素begin()返回指向第一个元素的迭代器end()返回指向最后一个元素之后位置的迭代器- 迭代器遍历:
for (p = v.begin(); p != v.end(); p++)
思路分析
输入 10 → 循环 1~10 → 筛选奇数 → push_back → 迭代器遍历输出。
易错点
end()不指向最后一个元素,而是指向最后一个元素之后- 循环条件应为
p != v.end(),不是p <= v.end()
🔄 举一反三
- 如果输入
n = 6,输出结果是什么?查看练习答案与解析
答案:
1 3 5解析:循环i从 1 到 6,奇数有 1、3、5,添加到 vector 后输出。
- 阅读该程序,给出程序的输出结果。
#include <iostream>
using namespace std;
class Sample
{
private:
int i;
static int count;
public:
Sample();
void display();
};
Sample::Sample()
{
i = 0;
count++;
}
void Sample::display()
{
cout << "i=" << i++ << ",count=" << count << endl;
}
int Sample::count = 0;
int main()
{
Sample a, b;
a.display();
b.display();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
查看答案与解析
答案:
i=0,count=2
i=0,count=22
解析:
第一步:分析静态成员 count
count是静态成员变量,属于类本身,被所有对象共享。- 初始值:
int Sample::count = 0; - 创建
a时:count++→count = 1 - 创建
b时:count++→count = 2 - 最终
count = 2
第二步:分析 a.display()
a.i = 0(构造函数中初始化为 0)- 输出
i=0,然后i++(后置自增,输出后a.i变为 1) count = 2(静态成员,所有对象共享)- 输出:
i=0,count=2
第三步:分析 b.display()
b.i = 0(构造函数中初始化为 0,注意b.i和a.i是独立的)- 输出
i=0,然后i++(输出后b.i变为 1) count = 2(仍然是 2,因为 display 没有修改 count)- 输出:
i=0,count=2
第四步:关键知识点
- 静态成员
count被所有对象共享,构造了两个对象后值为 2 - 普通成员
i每个对象独立拥有一份 i++是后置自增,先输出当前值再自增
难度: ⭐⭐ 考点: #静态成员 #static #后置自增 #对象构造
💡 学习锦囊
📖 相关公式与知识点:
- 静态成员变量:
static int count;——属于类,所有对象共享 - 静态成员初始化:必须在类外进行,如
int Sample::count = 0; - 普通成员变量:每个对象独立拥有一份
i++(后置自增):先使用当前值,再自增++i(前置自增):先自增,再使用新值
思路分析
静态成员 → 所有对象共享 → 构造两次 count=2 → display 中 i++ 是后置 → 输出时 i 还是 0。
易错点
- 认为
a.display()后count会变化——display()没有修改count - 混淆
i++和++i——后置先输出再自增
🔄 举一反三
- 如果将
i++改为++i,输出结果是什么?查看练习答案与解析
答案:
i=1,count=2 i=1,count=21
2解析:
++i是前置自增,先自增再输出。a.i从 0 变为 1 后输出,b.i同理。两次输出i=1。
四、编程题(每题 15 分,总计 30 分)
- 按下列要求编程,实现类的定义,并在主函数中测试这个类。
定义一个描述日期的类 Date,包括的数据成员有年(year)、月(month)和日(day),并实现如下功能函数: (1)日期对象初始化; (2)设置日期; (3)以 year/month/day 形式输出日期; (4)判断闰年。
查看答案与解析
答案:
#include <iostream>
using namespace std;
class Date
{
private:
int year, month, day;
public:
// (1) 日期对象初始化(构造函数,带默认参数)
Date(int y = 1900, int m = 1, int d = 1)
{
year = y;
month = m;
day = d;
}
// (2) 设置日期
void setDate(int y, int m, int d)
{
year = y;
month = m;
day = d;
}
// (3) 以 year/month/day 形式输出日期
void outputDate()
{
cout << year << "/" << month << "/" << day << endl;
}
// (4) 判断闰年
bool isLeapYear()
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
};
int main()
{
Date d; // 使用默认参数构造
d.outputDate(); // 输出默认日期
d.setDate(2015, 3, 5); // 设置新日期
d.outputDate(); // 输出新日期
if (d.isLeapYear())
cout << "a leap year." << endl;
else
cout << "not a leap year." << endl;
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
解析:
第一步:设计数据成员 日期需要三个私有数据成员:year、month、day。
第二步:实现构造函数 使用默认参数,方便创建对象时初始化:Date(int y = 1900, int m = 1, int d = 1)
第三步:实现设置日期函数setDate(int y, int m, int d) 修改三个数据成员的值。
第四步:实现输出函数 按 year/month/day 格式输出,使用 / 分隔。
第五步:实现闰年判断 闰年规则:
- 能被 4 整除但不能被 100 整除,或者
- 能被 400 整除
- 表达式:
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
难度: ⭐⭐ 考点: #类定义 #构造函数 #默认参数 #闰年判断
💡 学习锦囊
📖 相关公式与知识点:
- 类的基本结构:
class ClassName { private: ... public: ... }; - 构造函数带默认参数:
Date(int y = 1900, int m = 1, int d = 1) - 闰年判断公式:
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) - 成员函数在类外定义时使用
ClassName::funcName()
思路分析
类的设计步骤:确定数据成员 → 设计构造函数 → 实现各功能函数 → 编写主函数测试。
易错点
- 闰年判断条件写错——能被 100 整除但不能被 400 整除的不是闰年(如 1900 年)
- 忘记在类定义末尾加分号
};
🔄 举一反三
- 为 Date 类添加一个成员函数
int getDaysInMonth(),返回当前月份的天数。查看练习答案与解析
答案:
cppint getDaysInMonth() { int days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; if (month == 2 && isLeapYear()) return 29; return days[month]; }1
2
3
4
5
6
7解析:使用数组存储每月天数,2 月特殊处理(闰年 29 天,平年 28 天)。数组索引 0 用 0 占位,方便用月份直接索引。
- 根据下列 Vector 类定义,编程完成 Vector 类的具体实现:
class Vector
{
friend ostream& operator<<(ostream& out, const Vector& v);
private:
int* data;
int size;
public:
Vector();
Vector(int a[], int n);
Vector(const Vector& s);
~Vector();
Vector& operator=(const Vector& v);
int& operator[](int index);
};2
3
4
5
6
7
8
9
10
11
12
13
14
查看答案与解析
答案:
#include <iostream>
using namespace std;
// 默认构造函数
Vector::Vector()
{
data = NULL;
size = 0;
}
// 带参构造函数
Vector::Vector(int a[], int n)
{
size = n;
data = new int[size];
for (int i = 0; i < size; i++)
data[i] = a[i];
}
// 拷贝构造函数(深拷贝)
Vector::Vector(const Vector& s)
{
size = s.size;
data = new int[size];
for (int i = 0; i < size; i++)
data[i] = s.data[i];
}
// 析构函数
Vector::~Vector()
{
if (data != NULL)
delete[] data;
}
// 赋值运算符重载(深赋值)
Vector& Vector::operator=(const Vector& v)
{
if (this == &v)
return *this;
if (size != v.size)
{
delete[] data;
size = v.size;
data = new int[size];
}
for (int i = 0; i < size; i++)
data[i] = v.data[i];
return *this;
}
// 下标运算符重载
int& Vector::operator[](int index)
{
return data[index];
}
// 流插入运算符重载(友元函数)
ostream& operator<<(ostream& out, const Vector& v)
{
for (int i = 0; i < v.size; i++)
out << v.data[i] << " ";
return out;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
解析:
第一步:分析类的结构 Vector 类包含两个私有成员:int* data(动态数组指针)和 int size(数组大小)。由于包含指针成员,需要实现深拷贝和深赋值。
第二步:各函数实现要点
| 函数 | 关键点 |
|---|---|
| 默认构造函数 | data = NULL; size = 0; |
| 带参构造函数 | 分配 new int[n],逐元素复制 |
| 拷贝构造函数 | 深拷贝:分配新内存,逐元素复制 |
| 析构函数 | delete[] data 释放动态内存 |
operator= | 检查自赋值、释放旧内存、分配新内存、逐元素复制 |
operator[] | 返回 data[index] 的引用,支持读写 |
operator<< | 友元函数,遍历输出所有元素 |
第三步:深拷贝与浅拷贝的区别
- 浅拷贝(默认):
data = v.data——两个对象指向同一内存 - 深拷贝(自定义):分配新内存并复制内容——两个对象独立
第四步:赋值运算符的三步曲
- 检查自赋值:
if (this == &v) return *this; - 释放旧资源:
delete[] data; - 分配新资源并复制:
data = new int[size];+ 逐元素复制
难度: ⭐⭐⭐ 考点: #深拷贝 #运算符重载 #动态内存管理 #友元函数
💡 学习锦囊
📖 相关公式与知识点:
- 深拷贝三要素:拷贝构造 + 赋值运算符 + 析构函数(Rule of Three)
- 赋值运算符步骤:自赋值检查 → 释放旧资源 → 分配新资源 → 复制数据
operator[]返回引用:支持v[i] = x;的赋值操作- 友元函数
operator<<:第一个参数是ostream&,不能是成员函数
思路分析
类有指针成员 → Rule of Three → 拷贝构造(深拷贝)+ 赋值运算符(深赋值)+ 析构函数(释放内存)。
易错点
- 忘记自赋值检查——
v = v;时可能先释放再访问已释放的内存 operator=忘记释放旧内存——导致内存泄漏operator[]不返回引用——无法实现v[i] = x;
🔄 举一反三
- 如果 Vector 类增加一个成员函数
void push_back(int val),如何实现?查看练习答案与解析
答案:
cppvoid push_back(int val) { int* newData = new int[size + 1]; for (int i = 0; i < size; i++) newData[i] = data[i]; newData[size] = val; delete[] data; data = newData; size++; }1
2
3
4
5
6
7
8
9
10解析:分配比原来大 1 的新数组 → 复制旧数据 → 添加新元素 → 释放旧数组 → 更新指针和大小。这是动态数组扩容的基本操作。